// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package generators

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"

	"golang.org/x/benchmarks/sweet/common"
	"golang.org/x/benchmarks/sweet/common/fileutil"
	"golang.org/x/benchmarks/sweet/harnesses"

	osi "github.com/opencontainers/runtime-spec/specs-go"
)

// GVisor is a dynamic assets Generator for the gvisor benchmark.
type GVisor struct{}

// Generate builds binaries for workloads that will run under gVisor
// as part of the benchmark. The sources for these workloads live in
// the source assets directory and are relatively short Go programs.
//
// It also copies over static assets which are necessary to run the
// benchmarks.
func (GVisor) Generate(cfg *common.GenConfig) error {
	goTool := *cfg.GoTool
	goTool.Env = goTool.Env.MustSet("CGO_ENABLED=0") // Disable CGO for workloads.

	// Build workload sources into binaries in the output directory,
	// with one binary for each supported platform.
	workloads := []string{
		"http",
		"syscall",
	}
	for _, workload := range workloads {
		workloadSrcDir := filepath.Join(cfg.SourceAssetsDir, workload)
		workloadOutDir := filepath.Join(cfg.OutputDir, workload)
		if err := os.MkdirAll(workloadOutDir, 0755); err != nil {
			return err
		}
		for _, p := range common.SupportedPlatforms {
			// Generate the output directory.
			platformDirName := fmt.Sprintf("%s-%s", p.GOOS, p.GOARCH)
			workloadBinOutDir := filepath.Join(workloadOutDir, "bin", platformDirName)
			if err := os.MkdirAll(workloadBinOutDir, 0755); err != nil {
				return err
			}
			goTool := common.Go{Tool: goTool.Tool, Env: p.BuildEnv(goTool.Env)}

			// Build the workload.
			err := goTool.BuildPath(workloadSrcDir, filepath.Join(workloadBinOutDir, "workload"))
			if err != nil {
				return fmt.Errorf("building workload %s for %s: %v", workload, p, err)
			}
		}
	}

	// In order to regenerate startup/config.json, we require a working
	// copy of runsc. Get and build it from the harness.
	//
	// Create a temporary directory where we can put the gVisor source.
	tmpDir, err := os.MkdirTemp("", "gvisor-gen")
	if err != nil {
		return err
	}
	srcDir := filepath.Join(tmpDir, "src")
	if err := os.MkdirAll(srcDir, os.ModePerm); err != nil {
		return err
	}
	if err := (harnesses.GVisor{}).Get(&common.GetConfig{SrcDir: srcDir}); err != nil {
		return err
	}

	// Ensure the startup subdirectory exists.
	if err := os.MkdirAll(filepath.Join(cfg.OutputDir, "startup"), 0755); err != nil {
		return err
	}

	// Build the runsc package in the repository. CGO_ENABLED must be 0.
	// See https://github.com/google/gvisor#using-go-get.
	cfg.GoTool.Env = cfg.GoTool.Env.MustSet("CGO_ENABLED=0")
	runscBin := filepath.Join(tmpDir, "runsc")
	if err := cfg.GoTool.BuildPath(filepath.Join(srcDir, "runsc"), runscBin); err != nil {
		return err
	}

	// Delete config.json if it already exists, because runsc
	// will fail otherwise.
	specFile := filepath.Join(cfg.OutputDir, "startup", "config.json")
	if err := os.Remove(specFile); err != nil && !errors.Is(err, os.ErrNotExist) {
		return err
	}

	// Generate config.json.
	cmd := exec.Command(runscBin, "spec")
	cmd.Dir = filepath.Join(cfg.OutputDir, "startup")
	if err := cmd.Run(); err != nil {
		return err
	}

	// Mutate the config.json slightly for our purposes and write it back out.
	specBytes, err := os.ReadFile(specFile)
	if err != nil {
		return err
	}
	var spec osi.Spec
	if err := json.Unmarshal(specBytes, &spec); err != nil {
		return err
	}
	spec.Process.Terminal = false
	spec.Process.Args = []string{"/hello"}
	var buf bytes.Buffer
	enc := json.NewEncoder(&buf)
	enc.SetIndent("", "    ")
	if err := enc.Encode(&spec); err != nil {
		return err
	}
	if err := os.WriteFile(specFile, buf.Bytes(), 0666); err != nil {
		return err
	}

	// Everything below this point is static assets. If we're in the
	// same directory, just stop here.
	if cfg.AssetsDir == cfg.OutputDir {
		return nil
	}

	// Generate additional directory structure for static assets
	// that isn't already generated by the build process above.
	if err := os.MkdirAll(filepath.Join(cfg.OutputDir, "http", "assets"), 0755); err != nil {
		return err
	}

	// Copy static assets over.
	staticAssets := []string{
		filepath.Join("http", "assets", "gopherhat.jpg"),
		filepath.Join("http", "assets", "gophermega.jpg"),
		filepath.Join("http", "assets", "gopherswim.jpg"),
		filepath.Join("http", "assets", "gopherhelmet.jpg"),
		filepath.Join("http", "assets", "gopherrunning.jpg"),
		filepath.Join("http", "assets", "gopherswrench.jpg"),
		filepath.Join("http", "README.md"),
		filepath.Join("startup", "README.md"),
		filepath.Join("syscall", "README.md"),
	}
	if err := copyFiles(cfg.OutputDir, cfg.AssetsDir, staticAssets); err != nil {
		return err
	}

	// As a special case, copy everything under startup/rootfs.
	// It's a rootfs, so enumerating everything here would be tedious
	// and not really useful.
	//
	// TODO(mknyszek): Generate this directory from a container image.
	// There's some complications to this, because Cloud Build runs
	// inside docker, and this is generated from a docker image.
	return fileutil.CopyDir(
		filepath.Join(cfg.OutputDir, "startup", "rootfs"),
		filepath.Join(cfg.AssetsDir, "startup", "rootfs"),
		nil,
	)
}
